// Copyright 2012 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// TODO: thread safety
// TODO: IPv6 support

package net

import (
	"os"
	"syscall"
	"time"
)

type SockStatus int

const (
	_INVALID = SockStatus(iota)
	_UNCONNECTED
	_CONNECTED
	_CLOSED
)

type UDPConn struct {
	status   SockStatus
	socketID int
	laddr    *UDPAddr
	raddr    *UDPAddr
}

func (addr *UDPAddr) toSockAddr4() *syscall.SockAddrIn {
	ip4 := addr.IP.To4()
	if ip4 == nil {
		return nil
	}
	port := addr.Port

	return &syscall.SockAddrIn{
		Family: uint16(syscall.AF_INET),
		Port:   uint16(port),
		Addr:   [4]byte{ip4[0], ip4[1], ip4[2], ip4[3]},
	}
}

func (addr *UDPAddr) toSockAddr6() *syscall.SockAddrIn6 {
	ip6 := addr.IP.To16()
	if ip6 == nil {
		return nil
	}
	port := addr.Port

	return &syscall.SockAddrIn6{
		Family:   uint16(syscall.AF_INET6),
		Port:     uint16(port),
		FlowInfo: 0,
		Addr: [16]byte{ip6[0], ip6[1], ip6[2], ip6[3],
			ip6[4], ip6[5], ip6[6], ip6[7],
			ip6[8], ip6[9], ip6[10], ip6[11],
			ip6[12], ip6[13], ip6[14], ip6[15]},
		ScopeID: 0,
	}
}

func sockAddrToUDPAddr(sockaddr syscall.SockAddr) *UDPAddr {
	switch addr := sockaddr.(type) {
	case *syscall.SockAddrIn:
		a := make([]byte, 4)
		copy(a, addr.Addr[:])
		return &UDPAddr{IP(a), int(addr.Port)}
	case *syscall.SockAddrIn6:
		a := make([]byte, 16)
		copy(a, addr.Addr[:])
		return &UDPAddr{IP(a), int(addr.Port)}
	}
	return nil
}

func DialUDP(net string, laddr, raddr *UDPAddr) (*UDPConn, error) {
	switch net {
	case "udp", "udp4", "udp6":
	default:
		return nil, UnknownNetworkError(net)
	}

	if raddr == nil {
		return nil, &OpError{"dial", net, nil, errMissingAddress}
	}

	if ip4 := raddr.IP.To4(); ip4 != nil {
		sid, err := syscall.Socket(syscall.PF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_UDP)
		if err != nil {
			return nil, &OpError{"socket", net, nil, err}
		}

		if laddr != nil {
			err = syscall.Bind(sid, laddr.toSockAddr4())
			if err != nil {
				return nil, &OpError{"bind", net, laddr, err}
			}
		}

		err = syscall.Connect(sid, raddr.toSockAddr4())
		if err != nil {
			return nil, &OpError{"connect", net, raddr, err}
		}

		return &UDPConn{
			status:   _CONNECTED,
			socketID: sid,
			laddr:    laddr,
			raddr:    raddr,
		}, nil
	}

	// TODO: support IPv6
	return nil, ip6NotSupportedYet
}

func ListenMulticastUDP(net string, ifi *Interface, gaddr *UDPAddr) (*UDPConn, error) {
	// TODO
	return nil, helenosSupportError
}

func ListenUDP(net string, laddr *UDPAddr) (*UDPConn, error) {
	switch net {
	case "udp", "udp4", "udp6":
	default:
		return nil, UnknownNetworkError(net)
	}

	if ip4 := laddr.IP.To4(); ip4 != nil {
		sid, err := syscall.Socket(syscall.PF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_UDP)
		if err != nil {
			return nil, &OpError{"socket", net, nil, err}
		}

		err = syscall.Bind(sid, laddr.toSockAddr4())
		if err != nil {
			return nil, &OpError{"bind", net, laddr, err}
		}

		return &UDPConn{
			status:   _UNCONNECTED,
			socketID: sid,
			laddr:    laddr,
			raddr:    nil,
		}, nil
	}

	// TODO: support IPv6
	return nil, ip6NotSupportedYet
}

func ListenUnixgram(net string, laddr *UnixAddr) (*UDPConn, error) {
	return nil, helenosSupportError
}

func (c *UDPConn) Close() error {
	c.status = _CLOSED
	return syscall.CloseSocket(c.socketID)
}

func (c *UDPConn) File() (f *os.File, err error) {
	return nil, helenosSupportError
}

func (c *UDPConn) LocalAddr() Addr {
	return c.laddr
}

func (c *UDPConn) RemoteAddr() Addr {
	return c.raddr
}

func (c *UDPConn) Read(b []byte) (int, error) {
	if c.status != _CONNECTED {
		return 0, syscall.EINVAL
	}

	return syscall.Recv(c.socketID, b, 0)
}

func (c *UDPConn) Write(b []byte) (int, error) {
	if c.status != _CONNECTED {
		return 0, syscall.EINVAL
	}

	return syscall.Send(c.socketID, b, 0)
}

func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err error) {
	if c.status != _UNCONNECTED {
		return 0, nil, syscall.EINVAL
	}

	n, fromaddr, err := syscall.RecvFrom(c.socketID, b, 0)
	return n, sockAddrToUDPAddr(fromaddr), err
}

func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error) {
	if c.status != _UNCONNECTED {
		return 0, syscall.EINVAL
	}

	return syscall.SendTo(c.socketID, b, 0, addr.toSockAddr4())
}

func (c *UDPConn) ReadFrom(b []byte) (int, Addr, error) {
	return c.ReadFromUDP(b)
}

func (c *UDPConn) WriteTo(b []byte, addr Addr) (int, error) {
	if _, ok := addr.(*UDPAddr); !ok {
		return 0, syscall.EINVAL
	}
	return c.WriteToUDP(b, addr.(*UDPAddr))
}

// TODO: implement read deadlines; with UDP, write deadline is not that important 

func (c *UDPConn) SetDeadline(t time.Time) error {
	return helenosSupportError
}

func (c *UDPConn) SetReadDeadline(t time.Time) error {
	return helenosSupportError
}

func (c *UDPConn) SetWriteDeadline(t time.Time) error {
	return helenosSupportError
}

func (c *UDPConn) SetReadBuffer(bytes int) error {
	return helenosSupportError
}

func (c *UDPConn) SetWriteBuffer(bytes int) error {
	return helenosSupportError
}
